import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import qs from 'query-string'
import Canceler from './canceler'
import { genUniqueReqKey } from './utils'
import type { HttpOptions, RequestOptions, ResponseBody } from './types'
import { ContentTypeEnum, MethodEnum } from './types'

const noop = () => {}
const now = () => Date.now()

const appendUrl = (url: string, data: Record<string, any>) => {
  let queryStr = ''
  const dataKeys = Object.keys(data)
  if (dataKeys.length > 0) {
    dataKeys.forEach((key, index) => {
      queryStr += `${key}=${data[key]}${
        index < dataKeys.length - 1 ? '&' : ''
      }`
    })
  }
  if (queryStr)
    return `${url}${url.includes('?') ? '&' : '?'}${queryStr}`

  return url
}

export class Request {
  private pendingList: Map<string, unknown>
  private instance: AxiosInstance
  private options: HttpOptions

  constructor(options: HttpOptions) {
    this.pendingList = new Map<string, unknown>()
    this.options = options
    this.instance = axios.create(options?.config)
    this.setupInterceptors()
  }

  private setupInterceptors() {
    const { options, instance } = this
    instance.interceptors.request.use((config: RequestOptions) => {
      Canceler.add(config)
      // GET 请求前追加时间戳，防止请求缓存
      if (config.method === MethodEnum.GET) {
        config.url = appendUrl(config.url!, {
          _t: now(),
        })
      }

      if (options.requestInterceptor)
        config = options.requestInterceptor(config)

      return config
    }, options.requestInterceptorCatch ?? noop)

    instance.interceptors.response.use((response: AxiosResponse) => {
      Canceler.remove(response.config)

      if (options.responseInterceptor)
        response = options.responseInterceptor(response)

      return response
    }, options.responseInterceptorCatch ?? noop)
  }

  private static processForm(config: AxiosRequestConfig) {
    config.headers = config.headers || {}
    config.headers['Content-Type'] = ContentTypeEnum.FORM_URLENCODED
    config.data = qs.stringify(config.data)
  }

  private fetch<T, R=ResponseBody<T>>(options: RequestOptions): Promise<T> {
    const mergedOptions: RequestOptions = {
      ...this.options.config,
      ...options,
    }
    mergedOptions.form && Request.processForm(mergedOptions)

    const uniqueKey = genUniqueReqKey(mergedOptions)

    if (this.pendingList.has(uniqueKey))
      return this.pendingList.get(uniqueKey) as Promise<T>

    const promise = new Promise<T>((resolve, reject) => {
      this.instance.request<R>(mergedOptions)
        .then((response) => {
          const resp = (response.data as unknown) as ResponseBody<T>
          if (this.options.responseCallback)
            this.options.responseCallback(resp, resolve, reject, mergedOptions)
          else
            resolve(resp.data)
        })
        .catch((error) => {
          reject(error)
        })
        .finally(() => {
          this.pendingList.delete(uniqueKey)
        })
    })

    this.pendingList.set(uniqueKey, promise)

    return promise
  }

  get<T>(url: string, options?: RequestOptions) {
    return this.fetch<T>({
      ...(options || {}),
      method: MethodEnum.GET,
      url,
      params: options ? options.query : {},
    })
  }

  post<T>(url: string, options?: RequestOptions) {
    return this.fetch<T>({
      ...(options || {}),
      url: appendUrl(url, options?.query || {}),
      method: MethodEnum.POST,
      data: options ? options.param : {},
    })
  }

  upload<T>(file: File) {
    const formData = new FormData()
    formData.append('file', file)

    return this.fetch<T>({
      url: import.meta.env.VITE_UPLOAD_URL,
      method: MethodEnum.POST,
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data;charset=UTF-8',
      },
      withCredentials: false,
    })
  }
}
